今天會產生兩份 ELF:核心的 kernel.elf,以及使用者程式的 user.elf。
核心要把後者的機器碼放進一頁 RAM,映射到固定的使用者位址,再準備另一頁堆疊。
這篇先走到「已載入並可檢查」的位置,尚不執行 sret 進入 U-mode。
真正的權限轉換與系統呼叫會在 Day 17 完成,這樣程式載入和 Trap 返回的問題可以分開驗證。
《Operating System Concepts》第 10 版第 2.5 節,區分來源程式、連結後的執行檔,以及載入記憶體的執行映像。
我們昨天已經能把 VA 映射到 PA,現在可以讓使用者程式以固定 VA 連結,實際 backing page 則由配置器決定。
本次採用《OS in 1,000 Lines》〈應用程式〉與〈使用者模式〉的 raw binary 嵌入方法。
這個載入器有明確限制:只有一頁唯讀可執行映像,程式使用手寫組語,沒有 .bss、heap 或動態連結需求。
因此不需要在核心中解析 ELF program headers,但也不能直接拿一般 Linux 執行檔套用。
| 虛擬範圍 | 權限 | 用途 |
|---|---|---|
0x01000000~0x01000fff |
U、R、X | 使用者程式與唯讀文字 |
0x01001000~0x01001fff |
未映射 | 堆疊下方的 guard page |
0x01002000~0x01002fff |
U、R、W | 使用者堆疊 |
| 核心所需範圍 | 無 U 位元 | 核心程式、資料與核心堆疊 |
初始 user sp 是 0x01003000,第一次使用時往低位址保留空間。
Guard page 只能協助發現跨入該頁的存取,不能保證捕捉所有形式的堆疊溢位。
在 30-days-os-kernel/examples/tiny-kernel/ 執行:
make DAY=16
llvm-readelf -h build/day16/user.elf
llvm-objdump -d build/day16/user.elf
od -Ax -tx1 -N 64 build/day16/user.bin
完整來源為 user.S 與 user.ld。
Linker 將 user_start 放在 0x01000000,並檢查程式是否超出一頁。
Makefile 用 llvm-objcopy -O binary 取得 raw bytes,再透過 image.S 的 .incbin 嵌入核心唯讀區。
user_image_start:
.incbin USER_IMAGE
user_image_end:
USER_IMAGE 由 Makefile 指向本日的 user.bin,因此不必手動複製二進位內容。
核心用起訖符號算出大小,再逐 byte 複製到新配置的程式頁。
閱讀 user.c 的 user_demo(),載入部分包括:
memcpy(code, user_image_start, size);
vm_map(user_root, USER_BASE, (uint32_t) code, PTE_U | PTE_R | PTE_X);
vm_map(user_root, USER_STACK, (uint32_t) stack, PTE_U | PTE_R | PTE_W);
程式碼不可寫,堆疊不可執行,這是頁表權限真正開始區分用途的地方。
另外再配置一頁核心堆疊,給下一篇從 U-mode 進入核心時使用。
不能期待使用者提供的 sp 適合直接承接核心的 C 函式呼叫。
完成資料複製後執行 fence.i,使接下來取指令能與本 hart 的寫入保持一致。
它處理的是指令與資料的同步關係,和 sfence.vma 的位址轉譯同步不同。
執行:
make DAY=16 run
預期看到兩筆映射與準備完成訊息:
VA=0x1000000 vpn1=4 vpn0=0 offset=0
VA=0x1002000 vpn1=4 vpn0=2 offset=0
user image bytes=... prepared; entry=0x1000000 stack=0x1003000
experiment complete
程式還會檢查 guard page 不存在、程式頁沒有 W,以及堆疊頁沒有 X。
這些目前是頁表內容檢查,不是已經執行過違規存取的硬體保護測試。
可在 QEMU monitor 暫停後,使用 xp 讀取 dump 印出的程式 PA,與 od 的 raw bytes 比較。
這裡比較的是內容是否正確載入,不要把 VA 誤當成 xp 所需的實體位址。
Day 11~Day 13 展示核心行程切換,今天改用獨立開機路徑建立一個使用者位址空間。
把多個 U-mode 行程加入排程,還需要在切換時管理 satp、核心堆疊與每個行程的 Trap Frame。
目前先完成單一映像載入,之後閱讀 xv6 的 exec() 與 usertrapret() 時,再對照較完整的整合方式。
這也讓今天的可執行範圍保持明確,不會把只有兩個結構的狀態說成已完成多行程隔離。
本篇主要檔案是 user.S、user.ld、image.S、user.c 與 Makefile。
建議 commit 訊息:
day16: prepare user image and address space
Day 17 會使用這份映像,第一次進入 U-mode,再從 ecall 返回核心取得輸出服務。